前三篇介紹了網站架構、環境建立、域名系統等基礎知識。這次我們將深入探討 HTTP 協定中的 CRLF,理解其工作原理以及與網站安全之間的關係。
前後端之間需要透過請求封包和回應封包進行溝通,中間透過 HTTP 協定,請求封包和回應封包就是 HTTP 請求封包與 HTTP 回應封包。
curl nodelab.feifei.tw
使用 Windows 示範
使用 Linux 示範
思考: 為什麼沒有前面所介紹的請求封包和回應封包的標頭?
curl -i nodelab.feifei.tw
使用 Windows 示範
使用 Linux 示範
思考: 加上 -i 參數可以看到回應封包的標頭,還有其他參數嗎?
curl --help
curl -v nodelab.feifei.tw
使用 -v
查看過程
User-Agent
有 curl 的版本使用 Windows 示範
使用 Linux 示範
可以發現 Linux 參數更多
使用 curl -v nodelab.feifei.tw
指令
觀察 HTTP 請求和回應的詳細過程
* Host nodelab.feifei.tw:80 was resolved.
curl
首先需要將域名 nodelab.feifei.tw
解析成對應的 IP 位址,才能與伺服器建立連線。* IPv4: 172.104.100.165
可以看到,nodelab.feifei.tw
的 IP 位址為 172.104.100.165
。* Trying 172.104.100.165:80...
和 * Connected to nodelab.feifei.tw (172.104.100.165) port 80
curl
會嘗試與伺服器建立 TCP 連線。:80
代表使用的連接埠是 HTTP 的預設連接埠,表示這次是 HTTP 請求,而非 HTTPS。> GET / HTTP/1.1
、 > Host: nodelab.feifei.tw
、 > User-Agent: curl/8.7.1
、 > Accept: */*
、 >
curl
會發送 HTTP 請求給伺服器。請求封包由多個標頭組成,每個標頭佔一行,以 CRLF 結尾。>
符號表示這些是使用者端發送的封包。GET / HTTP/1.1
表示這是一個 GET 請求,請求取得根目錄 /
的資源,使用 HTTP/1.1 協定。Host: nodelab.feifei.tw
指定了請求的目標主機。User-Agent: curl/8.7.1
提供使用者端資訊,這裡顯示 curl
的版本。Accept: */*
表示使用者端可以接受任何類型的回應內容。* Request completely sent off
curl
確認請求封包已完整發送到伺服器。< HTTP/1.1 200 OK
、 < Server: nginx/1.18.0 (Ubuntu)
、 < Date: Wed, 18 Sep 2024 04:46:30 GMT
等
<
符號表示這些是伺服器回覆的封包。
HTTP/1.1 200 OK
表示請求成功。Server: nginx/1.18.0 (Ubuntu)
顯示伺服器使用的軟體和作業系統版本資訊。Date
, Content-Type
, Content-Length
, Connection
, X-Powered-By
, Accept-Ranges
, Cache-Control
, Last-Modified
, ETag
則提供了回應內容的相關資訊。<!DOCTYPE html>
CRLF 在 HTTP 通訊中扮演著重要的角色
根據 curl -v nodelab.feifei.tw
指令輸出結果的分析
CRLF 是 回車鍵 (CR, ASCII 13, \r) 和 換行符 (LF, ASCII 10, \n) 的組合,用於表示一行文字的結束。
▲ 歷史故事:以前打字機換行會有時間差,可能會造成傳過來的文字漏掉,因此發明兩個鍵,一「回車」,告知打字機將列印標頭定位於左邊的邊界;二「換行」,告知打字機將紙往下移動一行。
▲ 後來電腦發明之後,覺得結尾加上回車跟換行浪費空間,因此不同的作業系統有不同做法: Winodws 就是 \r\n (回車+換行),而 Linux 是只有 \n (換行),舊版 Mac OS: \r (回車)(現代 macOS 使用 \n(換行) 。
▲ 目前的編輯器多半能進行格式轉換。
在 HTTP 協定中,CRLF 用於分隔訊息的標頭 (Header) 和主體 (Body),以及標頭中的各個欄位。每個標頭欄位都以 CRLF 結尾,而標頭與主體之間使用兩個 CRLF 分隔。
以下是一個 HTTP 請求封包的範例:
GET / HTTP/1.1\r\n
Host: www.example.com\r\n
User-Agent: Mozilla/5.0\r\n
\r\n
This is the message body.
GET
)、請求路徑 (/
) 和 HTTP 版本 (HTTP/1.1
) 位於第一行,以 CRLF 結束。Host
、User-Agent
和 Accept
,每個欄位都以 CRLF 結束。curl --trace - nodelab.feifei.tw
--trace
生成二進位的追蹤日誌-
將追蹤日誌進行標準輸出(stdout),而不是寫入檔案回車鍵 (CR, ASCII 13, \r)
換行符 (LF, ASCII 10, \n)
Linux 示範
Windows 示範
這是什麼漏洞
這段程式碼存在一個CRLF(Carriage Return Line Feed)注入漏洞。CRLF 注入是一種攻擊者可以利用它在 HTTP 回應中插入惡意的標頭或內容的漏洞。
為什麼會有這個漏洞
漏洞的產生是因為在處理使用者提供的 url
參數時,未對其內容進行適當的驗證或過濾。
程式碼直接將使用者輸入的 url
值放入 HTTP 回應的標頭中,這使得攻擊者可以透過惡意的輸入(包含 0D 0A) 來操縱 HTTP 回應。
漏洞成因
url
參數進行任何形式的檢查,直接使用。Location
標頭中,沒有過濾特殊字符,如 CR(\r)和 LF(\n)。漏洞影響:
Set-Cookie
標頭,攻擊者可以設定或修改使用者的 Cookie,可能導致會話劫持。可參考 Perl 版本,可造成 XSS 與 Session 固定攻擊:
https://ithelp.ithome.com.tw/articles/10242682
// 設定重新導向的路由
app.get('/redirect', (req, res) => {
const url = req.query.url;
res.writeHead(302, { 'Location': url });
res.end();
});
使用 docker-compose up -d --build
TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["Location"]
/redirect?url=/%0D%0ASet-Cookie:feifei=good
%0D%0A
是 URL 編碼的 CR(\r)和 LF(\n),代表換行。Set-Cookie
標頭,以設定名為 feifei
、值為 good
的 Cookie。http
模組會自動檢查並阻止在 HTTP 標頭中插入 CR 和 LF 等非法字符。Error [ERR_INVALID_CHAR]: Invalid character in header content ["Location"]
。這是什麼漏洞
這段程式碼存在一個開放式重新導向漏洞(Open Redirect)。攻擊者可以利用該漏洞,將使用者從您信任的網站重新導向到惡意網站,進而可能進行釣魚攻擊或散播惡意軟體。
為什麼會有這個漏洞
漏洞的產生是因為在處理使用者提供的 url
參數時,未對其內容進行適當的驗證或限制。程式碼直接使用了使用者輸入的 url
值進行重新導向,這使得攻擊者可以指定任何網址,甚至是惡意網站。
漏洞成因
url
參數進行任何形式的檢查或限制,直接使用。漏洞影響
輸入惡意的 url
參數:
http://nodelab.feifei.tw/redirect?url=http://malicious-site.com
使用者將被重新導向到攻擊者控制的網站。
輸入驗證和過濾:對 url
參數進行嚴格的驗證和過濾,移除或轉義特殊字符。
app.get('/redirect', (req, res) => {
let url = req.query.url;
if (url && !/[\r\n]/.test(url)) {
res.writeHead(302, { 'Location': url });
res.end();
} else {
res.status(400).send('無效的重新導向網址');
}
});
使用內建的重新導向方法:使用框架提供的 res.redirect()
方法,這個方法會自動處理特殊字符並設定適當的標頭。
app.get('/redirect', (req, res) => {
const url = req.query.url;
res.redirect(302, url);
});
實施白名單策略:限制可以重新導向的 URL,僅允許預先定義的安全網址或相對路徑。
const allowedUrls = ['/home', '/profile', '/settings'];
app.get('/redirect', (req, res) => {
const url = req.query.url;
if (allowedUrls.includes(url)) {
res.redirect(302, url);
} else {
res.status(400).send('無效的重新導向網址');
}
});
URL 編碼:對輸入的 URL 進行編碼,確保特殊字符被正確處理。
const encodeUrl = require('encodeurl');
app.get('/redirect', (req, res) => {
const url = encodeUrl(req.query.url);
res.redirect(302, url);
});
移除協定相對的 URL:防止使用者使用 //malicious-site.com 這樣的協定相對 URL。
app.get('/redirect', (req, res) => {
const url = req.query.url;
if (url && !url.startsWith('//')) {
res.redirect(302, url);
} else {
res.status(400).send('無效的重新導向網址');
}
});
為了保障應用的安全性,開發者應該:
res.redirect()
而非手動設定標頭。在這篇文章中,深入探討了 HTTP 協定中的 CRLF(Carriage Return Line Feed)以及其在網路安全中的重要性。
透過使用 curl
指令,實作了如何模擬瀏覽器的請求和回應,並觀察了 HTTP 封包的細節,特別是 CRLF 在分隔標頭和主體中的作用。
還探討 CRLF 注入漏洞,分析了其成因和影響,並嘗試在一個範例程式碼中進行攻擊,觀察到 Node.js 的內建安全機制如何防止此類攻擊。
研究開放式重新導向(Open Redirect)漏洞,了解攻擊者如何利用未經驗證的輸入將使用者導向惡意網站,並提供了多種修復方法,包括輸入驗證、使用內建的重導方法、實施白名單策略和 URL 編碼。
最後,總結在開發中應該始終對使用者輸入進行驗證,利用框架提供的安全功能,了解常見的安全漏洞,並定期檢查和更新程式碼以保障應用的安全性。
題目一:在 HTTP 協定中,CRLF 的主要作用是什麼?
A) 分隔域名和路徑
B) 分隔標頭和主體,以及標頭中的各個欄位
C) 加密 HTTP 請求
D) 儲存 Cookie 資料
答案:B) 分隔標頭和主體,以及標頭中的各個欄位
題目二:CRLF 注入漏洞的主要成因是什麼?
A) 伺服器硬體故障
B) 使用者輸入未經適當驗證直接用於 HTTP 標頭
C) 網路連線不穩定
D) 加密協定不夠安全
答案:B) 使用者輸入未經適當驗證直接用於 HTTP 標頭
題目三:以下哪種方法不能有效防止開放式重新導向漏洞?
A) 對輸入的 URL 進行嚴格的驗證和過濾
B) 使用框架提供的 res.redirect()
方法
C) 實施白名單策略,限制可重導的 URL
D) 允許使用者輸入任意的重新導向 URL
答案:D) 允許使用者輸入任意的重新導向 URL
題目四:在 Node.js 中,哪一個內建機制可以防止 CRLF 注入攻擊?
A) 自動加密所有的 HTTP 請求
B) 檢查並阻止 HTTP 標頭中的非法字符
C) 強制所有連線使用 HTTPS
D) 自動更新所有的相依套件
答案:B) 檢查並阻止 HTTP 標頭中的非法字符
題目五:為了防止 CRLF 注入和開放式重新導向漏洞,開發者應該怎麼做?
A) 忽略使用者輸入,直接使用預設值
B) 將所有輸入都存入資料庫,以便日後分析
C) 對使用者輸入進行驗證和過濾,並使用安全的重新導向方法
D) 禁止所有的重新導向功能
答案:C) 對使用者輸入進行驗證和過濾,並使用安全的重新導向方法
步驟一:理解 CRLF 在 HTTP 中的作用
步驟二:使用 curl
觀察 HTTP 封包
curl -v
查看請求和回應的詳細資訊。curl --trace -
可以觀察到 CRLF 的實際運作。步驟三:實作可能存在漏洞的程式碼
server.js
中添加一個重新導向的路由,直接使用使用者的輸入:
app.get('/redirect', (req, res) => {
const url = req.query.url;
res.writeHead(302, { 'Location': url });
res.end();
});
步驟四:嘗試進行 CRLF 注入攻擊
/redirect?url=%0d%0aSet-Cookie:feifei%3Dgood
步驟五:理解開放式重新導向漏洞
步驟六:修復漏洞
res.redirect()
,讓框架處理安全性問題。修正後的程式碼範例:
app.get('/redirect', (req, res) => {
const url = req.query.url;
if (url && !url.startsWith('//') && !/[\r\n]/.test(url)) {
res.redirect(302, url);
} else {
res.status(400).send('無效的重新導向網址');
}
});
步驟七:驗證修復效果
步驟八:總結與反思